Redis
Administrator.DESKTOP-JLFNH9M
2020-08-06
Redis 简介 redis 就是一个数据库,不过与传统数据库不同的是 redis 的数据是存在内存中的,所以存写 速度非常快,因此 redis 被广泛应用于缓存方向 redis 也经常用来做分布式锁 为什么要用redis 高性能 第一次访问数据库某些数据会比较慢,因为要从磁盘上读取,将该用户访问的数据存在缓存里,下 次就可以直接从缓存中读取了,因为缓存在内存所以速度相当快 高并发 直接操作缓存能够承受的请求是远远大于直接访问数据库的 为什么要用 redis 而不用 map 做缓存? 缓存分为本地缓存和分布式缓存 自带的 map 实现的是本地缓存 最主要的特点是轻量以及快速,生命周期随着 jvm 的销毁而结束 缓存不具有一致性。 多实例的情况下 使用 redis 或 memcached 之类的称为分布式缓存 在多实例的情况下,各实例共用一份缓存数据,缓存具有一致性 redis 和 memcached 的区别 redis支持更丰富的数据类型 Redis支持数据的持久化,而Memecache把数据全部存在内存之中。 集群模式:memcached没有原生的集群模式,但是 redis 目前是原生支持 cluster 模式的. Memcached是多线程;Redis使用单线程 常见数据结构 String 常用命令: set,get,decr,incr,mget 等。 Hash 常用命令: hget,hset,hgetall 等。 适合用于存储对象,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值 List 常用命令: lpush,rpush,lpop,rpop,lrange等 Set 常用命令: sadd,spop,smembers,sunion 等 实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存 开销。 set 是可以自动排重的。 Sorted Set 常用命令: zadd,zrange,zrem,zcard等 增加了一个权重参数score,使得集合中的元素能够按score进行有序排列。 使用场所 比如:在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个 集合。Redis可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能。 举例: 在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息 (可以理解为按消息维度的消息排行榜)等信息,适合使用 Redis 中的 SortedSet 结构进行 存储。 删除机制 定期删除 redis默认是每隔 100ms 就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期 就删除 随机的原因 如 redis 存了几十万个 key ,每隔100ms就遍历所有的设置过期时间的 key 的话,就会给 CPU 带来很大的负载! 惰性删除 惰性删除可能会导致很多过期 key 到了时间并没有被删除掉 除非你的系统去查一下那个 key,才会被redis给删除掉 上面两种机制都有问题,假如没有删除到而且你也没检查到 内存会耗尽 redis 提供 6种数据淘汰策略 volatile-lru:设置过期时间的数据中挑选最近最少使用的淘汰 volatile-ttl:设置过期时间的数据中挑选将要过期的数据淘汰 volatile-random:设置过期时间的数据集中任意选择数据淘汰 allkeys-lru:内存不足的时候就移除最近最少使用的key(这个是最常用的). allkeys-random:从数据集中任意选择数据淘汰 no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。 这个应该没人使用吧 持久化机制 快照RDB 只追加文件AOF 创建快照保存某个时间点数据副本,对快照备份,可以将快照复制给其他服务器创建相同数据的 服务器副本(主从结构,提高redis性能),也可以留在原地重启服务器使用 redis.conf 与快照持久化相比,AOF持久化 的实时性更好,因此已成为主流的持久化方案。 默认情况下Redis没有开启AOF(append only file)方式的持久化 可以通过appendonly参数开启: appendonly yes AOF开启时,会在目录创建AOF文件,默认名是appendonly.aof 三种方式 appendfsync always 每个写入命令都调用fsync() 将数据刷回磁盘, 保证故障时候最多丢失一 个命令, 但是fsync是阻塞的 redis性能影响大 appendfsync everysec #每秒钟同步一次,显示地将多个写命令同步到硬盘 appendfsync no 永不调用fsync,由操作系统决定什么时候将缓冲区写入磁盘., 默认30S 为了兼顾数据和写入性能,用户可以考虑 appendfsync everysec选项 ,让Redis每秒同步一 次AOF文件,Redis性能几乎没受到任何影响 创建快照方法 BGSAVE命令 Redis会调用fork来创建一个子进程,然后子进程负责将数据先保存在临时文件里,而父进程 则继续处理命令请求。保存完毕后这个文件名会重命名 SAVE命令 接到SAVE命令的Redis服务器在快照创建完毕之前不会再响应任何其他命令。SAVE命令不常 重写/压缩AOF 某些key已经被删除或者过期,就不再写入到AOF文件里, 用户可以向Redis发送 BGREWRITEAOF命令 ,这个命令会通过移除AOF文件中的冗余命令来 重写(rewrite)AOF文件来减小AOF文件的体积 Redis 4.0 对于持久化机制的优化 Redis 4.0 开始支持 RDB 和 AOF 的混合持久化(默认关闭,可以通过配置项 aof-use-rdb- preamble 开启)。 redis将RDB作为AOF文件的开始部分,利用RDB能更快速重写和加载数据文件,提高启动速度, 之后就像传统的AOF在AOF文件记录写命令 事务 WATCH MULTI 标记事务开始 EXEC 标记事务阶数 监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务 将被打断。 缓存雪崩 简介:缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间 内承受大量请求而崩掉。 解决办法 事前:尽量保证整个 redis 集群的高可用性,发现机器宕机尽快补上。选择合适的内存淘汰策 略。 事中:数据库限流避免MySQL崩掉 事后:利用 redis 持久化机制保存的数据尽快恢复缓存 缓存穿透 简介:一般是黑客故意去请求缓存中不存在的数据,导致所有的请求都落到数据库上,造成数 据库短时间内承受大量请求而崩掉。 最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个 一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力 另一种 如果一个查询返回的数据为空(不管是数 据不存在,还是系统故障),我们仍然把这个空结果 进行缓存,但它的过期时间会很短,最长不超过五分钟。 如何解决 Redis 的并发竞争 Key 问题 问题也就是多个系统同时对一个 key 进行操作 如何保证缓存与数据库双写时的数据一致性? 底层数据结构 简单动态字符串(Simple dynamic string,SDS) // 字节数组,用于保存字符串 char buf[]; // 记录buf数组中已使用的字节数量,也是字符串的长度 int len; int free; // 记录buf数组未使用的字节数量 len属性记录了字符串的长度。那么获取字符串的长度时,时间复杂度只需要O(1)。 SDS不会发生溢出的问题 空间不足。先会扩展空间 底层数据结构 节点图解 list 结构 如果出现hash 值相同的情况怎么办?Redis 采用了链地址法,在相同key下面用单链表 字典 哈希表 节点 整个 跳跃表 完整结构 跳表是通过随机函数来维护“平衡性”。 当我们往跳表中插入数据的时候,我们可以通过一个随机函数,来决定这个结点插入到哪几级 索引层中,比如随机函数生成了值K,那我们就将这个结点添加到第一级到第K级这个K级索引 过程 redis会单独创建fork()一个子进程,将当前父进程的数据库数据复制到子进程的内存中,然后 由子进程写入到临时文件中,持久化的过程结束了,再用这个临时文件替换上次的快照文件, 然后子进程退出,内存释放。 过程 redis的分布式锁实现 分布式CAP原则 Consistency(一致性) Availability(可用性) Partition tolerance(分区容错性) 只能实现两个 redis轻量级MQ 如果用RabbitMQ就必须为它搭建一个服务器,同时如果要考虑可用性,就要为服务端建立一 个集群 中小型业务的开发过程中,可能业务的其他整个实现都没这个重。过重的组件服务会成倍增加 工作量。 Redis提供的list数据结构非常适合做消息队列。 使用Redis中list的操作BLPOP或BRPOP,即列表的阻塞式(blocking)弹出 BRPOP key [key ...] timeout 当给定列表内没有任何元素可供弹出的时候,连接将被 BRPOP 命令阻塞,直到等待超时或发 现可弹出元素为止。 当给定多个key参数时,按参数 key 的先后顺序依次检查各个列表,弹出第一个非空列表的尾 部元素。 列表的阻塞式弹出有两个特点 如果list中没有任务的时候,该连接将会被阻塞 连接的阻塞有一个超时时间,当超时时间设置为0时,即可无限等待,直到弹出消息 订阅/发布模式 消息A入队list的同时发布PUBLISH消息B到频道channel,此时已经订阅channel的worker就 接收到了消息B,知道了list中有消息A进入,即可循环lpop或rpop来消费list中的消息 Redis过期策略 实现原理 redis设置过期时间: expire key time(以秒为单位)--这是最常用的方式 三种过期策略 定时删除 含义:在设置key的过期时间的同时,为该key创建一个定时器,让定时器在key的过期时间来 临时,对key进行删除 优点:保证内存被尽快释放 缺点: 若过期key很多,删除这些key会占用很多的CPU时间,在CPU时间紧张的情况下,CPU不能把 所有的时间用来做要紧的事儿,还需要去花时间删除这些key 定时器的创建耗时,若为每一个设置过期时间的key创建一个定时器(将会有大量的定时器产 生),性能影响严重 懒汉式删除 含义:key过期的时候不删除,每次通过key获取值的时候去检查是否过期,若过期,则删除, 返回null。 优点 删除操作只发生在通过key取值的时候发生,而且只删除当前key,所以对CPU时间的占用是比 较少的,而且此时的删除是已经到了非做不可的地步 缺点 若大量的key在超出超时时间后,很久一段时间内,都没有被获取过,那么可能发生内存泄露 (无用的垃圾占用了大量的内存) 定期删除 含义:每隔一段时间执行一次删除过期key操作 优点: 通过限制删除操作的时长和频率,来减少删除操作对CPU时间的占用--处理"定时删除"的缺点 定期删除过期key--处理"懒汉式删除"的缺点 缺点: 会造成一定的内存占用,但是没有懒汉式那么占用内存 会定期的去进行比较和删除操作,cpu方面不如懒汉式,但是比定时好 优点 只有一个文件 dump.rdb 方便持久化 容灾性好,一个文件可以保存到安全磁盘 性能最化,fork子进程来完成写操作,让主进程继续处理命令 相对于数据集大时,比AOF启动效率高 缺点:数据安全性较低,不能提供很强的一致性 优点 指所有的命令行记录以redis,命令请求协议的格式保存为aof文件 数据安全,通过append模式写文件,即使中途服务器宕机可以通过redis-check-aof工具解决 数据一致性问题。3AOF机制的rewrite模式 缺点:1文件比RDB形式文件大。2数据集大比RDB启动效率低) 主从复制 缓存使用不当 为什么要使用缓存 高性能 高并发 如果有个操作经过多个sql语句,这个结果接下来一定时间都不会变了,走缓存的话 只需要走一个 数据库,之后请求都走缓存,性能提升 mysql承受不了短时间内大量请求,缓存是走内存的 可以支撑高并发 读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应 更新的时候,先删除缓存,然后更新数据库 更新缓存的代价有时候是很高的,每次修改数据库都要更新一次,但是这个缓存可能很少使用到, 这样就有大量冷数据,如果是删除缓存的话,重新计算就可以了,开销降低,用到缓存才去算缓存 原因是如果先更新数据库,再删除缓存,删除失败的话走缓存会出现不一致问题,如果更新失败,那 也是旧数据,和缓存一致 redis 采用异步方式复制数据到 slave 节点,不过 redis2.8 开始,slave node 会周期性地确 认自己每次复制的数据量 一个 master node 是可以配置多个 slave node 的; slave node 也可以连接其他的 slave node; slave node 做复制的时候,不会 block master node 的正常工作; 也不会 block 对自己的查询操作,它会用旧的数据集来提供服务;但是复制完成的时候,需要 删除旧数据集,加载新数据集,这个时候就会暂停对外服务了; slave node 主要用来进行横向扩容,做读写分离,扩容的 slave node 可以提高读的吞吐量 核心原理 当启动一个 slave node 的时候 它会发送一个 PSYNC 命令给 master node 完全重新同步,首次连接主节点的时候 此时 master 会启动一个后台线程,开始生成一份 RDB 快照文件,同时还会将从客户端 client 新收到的所有写命令缓存在内存中。 RDB 文件生成完毕后, master 会将这个 RDB 发送给 slave,slave 会先写入本地磁盘,然 后再从本地磁盘数据加载到内存中,接着 master 会将内存中缓存的写命令发送到 slave, slave 也会同步这些数据 图解 部分重新同步 从 redis2.8 开始,就支持主从复制的断点续传 网络连接断掉了,那么可以接着上次复制的地方,继续复制下去,而不是从头开始复制一份。 主节点会在内存中维护一个 backlog 缓冲区,根据这个缓冲区来决定是完全重新同步还是部分 重新同步 如果 master 和 slave 网络连接断掉了,slave 会让 master 从上次 offset 开始继续复制,如 果没有找到对应的 offset,那么就完全重新同步, 如果找到了但是断开期间接受的写入命令超 过缓冲区容量,也会完全重新同步 过期 key 处理 slave 不会过期 key,只会等待 master 过期 key 如果 master 过期了一个 key,或者通过 LRU 淘汰了一个 key,那么会模拟一条 del 命令发 送给 slave。 复制的完整流程 哨兵集群实现高可用 功能 集群监控:负责监控 主实例和从实例是否正常工作。 消息通知:如果某个 redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员。 故障转移:如果 master node 挂掉了,会自动转移到 slave node 上。 配置中心:如果故障转移发生了,通知 client 客户端新的 master 地址 哨兵用于实现 redis 集群的高可用,本身也是分布式的,作为一个哨兵集群去运行,互相协同 工作 判断一个 master node 是否宕机了,需要大部分的哨兵都同意才行 即使部分哨兵节点挂掉了,哨兵集群还是能正常工作的 核心知识 至少需要 3 个实例,来保证自己的健壮性 哨兵 + redis 主从的部署架构,是不保证数据零丢失的,只能保证 redis 集群的高可用性 redis 每接受到一个修改数据的命令时候会将命令放入操作系统维护的一个缓冲区, 命令先存入 到缓冲区中, 然后linux调用fsync() 完成,这是一个阻塞调用. redis服务器关闭的时候fsync() 会 显式调用,以确保缓冲区数据会刷回磁盘 fsync函数同步内存中所有已修改的文件数据到储存设备。 AOF文件大小可能会显著增大,拖慢redis启动速度 更新多次的key只需要保存最新的值即可 重写 原理 主进程创建一个子进程,创建新AOF保存重写结果,防止重写失败影响旧AOF文件. 父进程则继续 将命令追加到旧AOF, 并采用写时复制机制, 不会占用与父进程相同的内存,但子进程无法访问 新的数据.,解决方法是, 父进程将新的命令写入缓冲区, 等子进程完成新AOF文件后,就向父进程 发送信号,父进程将缓冲区命令写入新AOF里面,然后用新AOF替换旧AOF 一个redis实例和哨兵可以部署在同一个机器上 哨兵每秒想实例发送PING命令检查是否可达,如果超过30秒没有响应,就视它为下线, 实现过程 如果是主实例发生故障, 其中一个从实例会选为新主实例,其他从实例从这个主实例主从复制 为了获得从实例信息,哨兵每十秒会向所有实例发送INFO REPLICATION命令,来获取最新信息 哨兵之间通过频道通信,报告自身及监控主实例状态,只有订阅该频道就能发现其他哨兵信息 布隆过滤器 点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”。 布隆过滤器是一个 bit 向量或者说 bit 数组,长这样 将一个key通过 m 组hash函数得到m个哈希值, 然后在这m个哈希值对应的下标 标为1 当key通过m个哈希值得到的下标去查找数组, 如果m个都标为1 就表示可能存在, 如果没有m 个都标为1 就一定不存在 通过watch key , 如果watch的key发生变化 事务就不执行 如果多个写操作同时过来,100个写操作同时watch,则最终只会有一个成功,99个执行失 败,何解? 将所有需要对同一个key的请求进行入队操作,然后用一个消费者线程从队头依次读出请求, 并对相应的key进行操作。 这样对于同一个key的所有请求就都是顺序访问,正常逻辑下则不会有写失败的情况下产生 。 从而最大化写逻辑的总体效率。 从实例发送SLAVEOF命令(向某个实例发出复制)后,发送带有offset和主实例ID, 主实例校验发 来的ID和自己ID是否一致,然后检查在offset能否在自己的缓冲区找到,如果找得到就获得连接 断开期间所有的写命令,意味着部分重新同步, 如果接受到的写命令数量超过缓存,就只能完全 重新同步 为什么Redis那么快 1.数据存在于内存,基于内存的操作非常快 2.数据结构简单 3.单线程, 消除上下文切换和竞争,没有加锁释放锁操作 4.多路I/O复用,非阻塞IO 分布式锁 用到的命令 setnx(key, value):“set if not exits”,若该key-value不存在,则成功加入缓存并且返回 1,否则返回0。 get(key):获得key对应的value值,若不存在则返回nil。 getset(key, value):先获取key对应的value值,若不存在则返回nil,然后将旧的value更新为 新的value。 expire(key, seconds):设置key-value的有效期为seconds秒。 锁实现条件 互斥性。在任意时刻,只有一个客户端能持有锁。 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客 户端能加锁。 具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。 加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了 面试题 避免超卖可以用redis的队列,100个商品在队列中抢到才可以 加锁代码 jedis.set(String key, String value, String nxxx, String expx, int time) 第一个为key,我们使用key来当锁,因为key是唯一的。 第二个为value,我们传的是requestId,很多童鞋可能不明白,有key作为锁不就够了吗,为 什么还要用到value?原因就是我们在上面讲到可靠性时,分布式锁要满足第四个条件解铃还 须系铃人,通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时 候就可以有依据。requestId可以使用UUID.randomUUID().toString()方法生成。 第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我 们进行set操作;若key已经存在,则不做任何操作; 第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时 间由第五个参数决定。 第五个为time,与第四个参数相呼应,代表key的过期时间。 解锁代码 第一行Lua脚本代码eval将Lua当成一个命令去执行,首先获取锁对应的value值,检查是否与 requestId相等,如果相等则删除锁(解锁) 第二行代码,我们将Lua代码传到jedis.eval()方法里,并使参数KEYS[1]赋值为lockKey, ARGV[1]赋值为requestId。eval()方法是将Lua代码交给Redis服务端执行 多实例情况下用RedLock 1.获取当前时间戳 2.client尝试按照顺序使用相同的key,value获取所有redis服务的锁,在获取锁的过程中的获取 时间比锁过期时间短很多,这是为了不要过长时间等待已经关闭的redis服务。并且试着获取 下一个redis实例。 比如:TTL为5s,设置获取锁最多用1s,所以如果一秒内无法获取锁,就放弃获取这个锁,从而 尝试获取下个锁 3.client通过获取所有能获取的锁后的时间减去第一步的时间,这个时间差要小于TTL时间并且 至少有3个redis实例成功获取锁,才算真正的获取锁成功 4.如果成功获取锁,则锁的真正有效时间是 TTL减去第三步的时间差 的时间;比如:TTL 是 5s,获取所有锁用了2s,则真正锁有效时间为3s(其实应该再减去时钟漂移); 5.如果客户端由于某些原因获取锁失败,便会开始解锁所有redis实例;因为可能已经获取了小 于3个锁,必须释放,否则影响其他client获取锁 跳表是有序的 跳跃表的查找次数近似于层数,时间复杂度为O(logn),插入、删除也为 O(logn)